package server.network;

import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import network.DataHandleListener;
import network.Listener;

import server.gameplay.*;
import network.protocol.*;

/**
 * @author Erik
 * This is the part that handles all the data received by the Server.
 * the functions notifyDataToHandle, notifyPlayerConnected, notifyPlayerDisconnected is
 * called from the Server through the interface DataHandleListener
 */
public class ServerInterface extends Listener implements DataHandleListener {
	private static final int PORT = 42302;

	private static final long MORALE_REDUCEMENT_INTERVAL = 10000;
	private final Runnable serverThread;
	private final Thread t1;
	private Server server;

	private ServerPlayersPackage players;
	private GameUpdater gameUpdater;
	private SendUnitQue sUQ;
	private LoadMap curMap;

	private ArrayList<Building> buildings;
	private ArrayList<Unit> units;
	// is set to false when the game starts
	private boolean acceptConnections = true;

	// morale timer, when to reduce due to time...
	private long reduceMoraleTimer;
	private server.gameplay.TowerAttack twrAttack;

	public ServerInterface() {
		players = new ServerPlayersPackage();
		// the neutral player, must be in the players-list, always on index 0
		ServerPlayerPackage natural = new ServerPlayerPackage(null, ServerPlayerPackage.NEUTRAL_PLAYER);
		natural.setNickName("Natural");
		players.add(natural);

		gameUpdater = new GameUpdater(this);

		serverThread = new Server(PORT);
		server = (Server) serverThread;
		// add ServerInterface to the listener-list
		server.addListener(this);
		// init and start the server-thread
		t1 = new Thread(serverThread, "server");
		t1.start();

		// start the game-loop
		gameUpdater.updateGame();
	}

	public static void main(String argv[]) {
		// start everything
		new ServerInterface();
	}

	//a part of the initializing of the game
	private void prepUserInterface() {
		buildings = curMap.getBuildings();
		units = new ArrayList<Unit>();
		sUQ = new SendUnitQue(units, this);
		reduceMoraleTimer = System.currentTimeMillis();
		twrAttack = new TowerAttack(units, buildings);
	}

	public boolean send(ServerPlayerPackage player, String data, int packageID) {
		synchronized (this) {
			return server.send(player.getSocket(), data, packageID);
		}
	}

	public void broadcast(String data, int packageID) {
		synchronized (players) {
			for (Iterator<ServerPlayerPackage> it = players.iterator(); it.hasNext();)
				send(it.next(), data, packageID);
		}
	}

	public void broadcastAllExceptOne(ServerPlayerPackage theOne, String data, int packageID) {
		synchronized (players) {
			Iterator<ServerPlayerPackage> itr = players.iterator();
			while (itr.hasNext()) {
				if (!(itr.next().getSocket() == theOne.getSocket()))
					send(itr.next(), data, packageID);
			}
		}
	}

	public void disconnectClient(ServerPlayerPackage player) {
		if (player != null)
			server.disconnectClient(player.getSocket());
	}

	// someone connected
	public void notifyPlayerConnected(SocketChannel socket, int playerID) {

		if (acceptConnections) {
			ServerPlayerPackage p = new ServerPlayerPackage(socket, playerID);

			// send a list of the connected players to the new client
			server.send(socket, players.toPackage(), Protocol.UPDATE_PLAYERS);
			System.out.println(players.toPackage());

			players.add(p);
			// check & set admin
			players.setAdmin();

			// tell all the clients to add the new player
			broadcast(p.toPackage(),
					Protocol.PLAYER_CONNECTED);
		}
		// the game is running and we don't want new connections
		// send a special-alert-message which means that the client
		// should not look up the player-id in the player-array
		else
			server.send(
					socket,
					"[Warning]: The game is running, connection refused. Come back later..",
					Protocol.LOBBY_MESSAGE);
	}

	// someone disconnected
	public void notifyPlayerDisconnected(SocketChannel socket) {
		// if the game is running the players(player-array) can not be changes,
		// do not remove any players from the list.
		if (!acceptConnections)
			return;

		ServerPlayerPackage player = players.find(socket);
		assert player != null;
		if (player != null) {
			System.out.println("Player: " + player.getNickName()
					+ " Disconnected");
			players.remove(player);
			// select a new admin
			players.setAdmin();
			// tell all clients that a user left the game
			broadcast(player.toPackage(),
					Protocol.PLAYER_DISCONNECTED);
		}
	}

	//there is data to handle
	public void notifyDataToHandle(SocketChannel socket, String dataPackage,
			int packageID) {

		ServerPlayerPackage player = players.find(socket);
		assert player != null;
		if (player == null)
			return;
		try {

			// check what package we received
			switch (packageID) {
			//not implemented..
			case Protocol.BAD_PACKAGE:
				break;

			// A login-request was sent by the client, update the internal
			// players-list and broadcast to all the clients to do the same
			case Protocol.LOGIN: {
				System.out.println("[Server]: Player: " + player.getNickName()
						+ " changed name to: " + dataPackage);
				player.setNickName(dataPackage);
				// Makes sure the [Admin] tag is added if the player is an admin
				broadcast(
						(new PlayerPackage(player.getID(), player.getNickName()))
								.toPackage(), Protocol.PLAYER_CHANGED_NAME);
				break;
			}

				// a client sent a kick-request, check if the client is admin
				// and if a client with the name the admin wrote exist
			case Protocol.KICK_PLAYER: {
				if (player.isAdmin()) {
					//the player to kick
					ServerPlayerPackage p = players.find(dataPackage);
					if (p != null) {
						String res = "[Server]: Player: " + p.getNickName()
								+ " was kicked";
						send(p, "[Warning]: you was kicked by: " + player.getNickName(), Protocol.LOBBY_MESSAGE);
						disconnectClient(p);
						broadcast(res, Protocol.LOBBY_MESSAGE);
					} else
						send(player, "[Server]: Player: \"" + dataPackage
								+ "\" not found", Protocol.LOBBY_MESSAGE);
				}
				break;
			}

				// player wrote something, broadcast it
			case Protocol.LOBBY_MESSAGE: {
				System.out.println(player.getNickName() + ": " + dataPackage);
				broadcast(player.getNickName() + ": " + dataPackage,
						Protocol.LOBBY_MESSAGE);
				break;
			}

				// the client requested a brand new list of the current players
			case Protocol.UPDATE_PLAYERS: {
				server.send(socket, players.toPackage(),
						Protocol.UPDATE_PLAYERS);
				break;
			}

			//starts the game(if the map was found)
			//all clients need to have the map
			case Protocol.INIT_GAME: {
				if (player.isAdmin()) {
					System.out.println("initializing game..");
					try {
						curMap = new LoadMap(players, dataPackage);
					} catch (Exception e) {
						// something went terribly wrong when loading the map,
						// abort loading
						broadcast("[Server]: Map not found",
								Protocol.LOBBY_MESSAGE);
						e.printStackTrace();
						curMap = null;
						return;
					}
					prepUserInterface();
					broadcast(players.toPackage(), Protocol.UPDATE_PLAYERS);
					broadcast(dataPackage, Protocol.INIT_GAME);
					gameUpdater.startGame();
					// makes sure that no one else can connect
					acceptConnections = false;
				}
				break;
			}

			//a player want to convert a building,
			//check if its possible
			case Protocol.BUILDING_CONVERT: {
				ConvertPackage pck = new ConvertPackage(dataPackage);
				Building b = buildings.get(pck.getBuilding());
				Buildings proto = null;
				switch (pck.getType()) {
				case 'w':
					proto = new House();
					break;
				case 'a':
					proto = new Tower(0);
					break;
				case 'd':
					proto = new Armory();
					break;
				default:
					if (b.isUpgradeable()) {
						//upgrade building
						b.upgrade();
						ConvertPackage data = new ConvertPackage('s',
								b.getIndex());
						broadcast(data.toPackage(), Protocol.BUILDING_CONVERT);
					}
					return;

				}
				if (b.isConvertableTo(proto)) {
					//convert building
					b.convertTo(proto);
					ConvertPackage data = new ConvertPackage(pck.getType(),
							b.getIndex());
					broadcast(data.toPackage(), Protocol.BUILDING_CONVERT);
				}
				break;
			}

			//a player sent a attack-package. perform the attack and broadcast to all
			case Protocol.ATTACK: {
				AttackPackage attack = new AttackPackage(dataPackage);
				Building curBuilding = buildings.get(attack.getCurBuilding());
				Building targetBuilding = buildings.get(attack
						.getTargetBuilding());

				double quota = 0;
				//check how many units the player want to send
				switch (attack.getAmount()) {
				case 'a':
					quota = (3.0d) / (4.0d);
					break;
				case 'w':
					quota = 1.0d;
					break;
				case 'd':
					quota = (1.0d) / (4.0d);
					break;
				default:
					quota = (1.0d) / (2.0d);
				}

				//get the unit-walking-path and start simulate the attack. 
				if (player.equal(curBuilding.getOwner())) {
					if (curBuilding != null && targetBuilding != null) {
						attack.setAmount((int) (curBuilding.getResidents() * quota));

						// (feature)
						// TODO
						// Chrash will occure if attackbuilding and homebuilding
						// is the same.

						sUQ.add(curBuilding, targetBuilding, quota);
						//broadcast that an attack is launched
						broadcast(attack.toPackage(), Protocol.ATTACK);
					}
				}
				break;
			}

			default:
				//unknown package received.. do nothing
				send(player, "[Server]: Unknown package received. PackageID: "
						+ packageID, Protocol.LOBBY_MESSAGE);
				System.out
						.println("Unknown package received. PackageID: "
								+ packageID + " sent by: "
								+ player.getNickName() + ")");
			}
		} catch (Exception e) {
			//an error with the connection occurred, close down the client
			e.printStackTrace();
			send(player, "[Warning]: (Server-Error) " + e.getMessage(),
					Protocol.LOBBY_MESSAGE);
			return;
		}
	}

	public SendUnitQue getSUQ() {
		return sUQ;
	}

	public ArrayList<Building> getBuildings() {
		return buildings;
	}

	public void setBuildings(ArrayList<Building> buildings) {
		this.buildings = buildings;
	}

	public List<Unit> getUnits() {
		return units;
	}

	public ServerPlayersPackage getPlayers() {
		return players;
	}

	/**
	 * calculate the total population player p have
	 * @param p
	 */
	public void getTotalPop(ServerPlayerPackage p) {
		int sum = 0;
		int suppertSum = 0;
		Building b;
		for (Iterator<Building> itr = buildings.iterator(); itr.hasNext();) {
			b = itr.next();
			if (b.getOwner().equal(p)) {
				sum += b.getResidents();
				if (b.isHouse())		
					suppertSum += (1 + b.getLevel())*20;
						
			}
		}
		//--//TODO (?)
		for(Iterator<Unit> itr = units.iterator(); itr.hasNext();){
			if(itr.next().getOwner().equal(p))
				sum++;
		}
		p.setSupportedPopulation(suppertSum);
		p.setTotalPop(sum);
	}

	/**
	 * calculate how many armory-buildings player p have
	 * @param p
	 */
	public void getTotalArmorys(ServerPlayerPackage p) {
		Iterator<Building> itr = buildings.iterator();
		int sum = 0;
		Building b;
		while (itr.hasNext()) {
			b = itr.next();
			if (b.isArmory() && b.getOwner().equal(p))
				sum++;
		}
		p.setTotalArmorys(sum);
	}

	/**
	 * reduces the morale for all players. (timebased)
	 */
	public void reduceMorale() {
		if (System.currentTimeMillis() - this.reduceMoraleTimer > MORALE_REDUCEMENT_INTERVAL) {
			this.reduceMoraleTimer = System.currentTimeMillis();
			Iterator<ServerPlayerPackage> itr = players.iterator();
			while (itr.hasNext()) {
				itr.next().reduceMoraleByTime();
			}
		}
	}

	public TowerAttack getShots() {
		return twrAttack;
	}

	/**
	 * /check the winner-condition.(there is only one player left with unit(s) & buiding(s))
	 */
	public int playerWin() {
		int winner = GameUpdater.NO_WINNER;
		for (Iterator<ServerPlayerPackage> itrPlayer = players.iterator(); itrPlayer.hasNext();) {
			ServerPlayerPackage player = itrPlayer.next();
			if (player.getID() != PlayerPackage.NEUTRAL_PLAYER && player.getTotalPop() > 0) {
				for (Iterator<Building> itrBuilding = buildings.iterator(); itrBuilding.hasNext();) {
					if (itrBuilding.next().getOwner().equal(player)) {
						if (winner == GameUpdater.NO_WINNER) {
							winner = player.getID();
							break;
						} else
							return GameUpdater.NO_WINNER;
					}
				}
			}
		}
		return winner;
	}
}